package org.grayrabbit.cloud.core.telnet;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TelnetSession implements Runnable, TelnetServiceConstants {
	private TelnetPrintConfiguration printConfiguration;
	private TelnetSessionController controller;
	private TelnetSessionState sessionState;
	protected Socket socket;
	protected Thread current;
	protected InputStream input;
	protected OutputStream output;
	TelnetSessionCallbackImpl sessionCallback;
	StringBuffer currentBuffer = new StringBuffer();

	final int TELNET_STATE_IDLE = 0;

	final int TELNET_STATE_IAC = 1;

	final int TELNET_STATE_SB = 2;

	final int TELNET_STATE_SB_OPTIONS = 3;

	final int TELNET_STATE_WILL = 4;

	final int TELNET_STATE_DO = 5;

	final int TELNET_STATE_DONT = 6;

	final int TELNET_STATE_WONT = 7;

	int telnetState = 0;

	int command = 0;

	boolean subnegotiation = false;

	int subnegotiationOption = 0;

	int subnegotiationOptionPart = 0;

	boolean escape = false;

	boolean escapeSequence = false;

	boolean suppressOutput = false;
	int minOff;
	int off;
	boolean newLine = true;

	public TelnetSession() {
	}

	TelnetSession(TelnetPrintConfiguration printConfiguration,
			TelnetSessionController controller, Socket socket) {
		this.printConfiguration = printConfiguration;
		this.controller = controller;
		this.sessionCallback = new TelnetSessionCallbackImpl();
		this.controller.setSessionCallback(this.sessionCallback);
		this.controller.setPrintConfiguration(this.printConfiguration);
		this.socket = socket;
		this.sessionState = this.controller.createSessionState();
	}

	public void start() throws TelnetServiceException {
		this.current = new Thread(this);
		this.current.setName("Telnet Connection");
		try {
			this.input = this.socket.getInputStream();
			this.output = this.socket.getOutputStream();

			setupTelnet();

			this.controller.notifyConnectionEstablished();

			this.current.start();
		} catch (Exception e) {
			String msg = "Connection could not be established "
					+ this.socket.getRemoteSocketAddress();
			TelnetServiceException tse = new TelnetServiceException(msg);
			throw ((TelnetServiceException) tse.fillInStackTrace());
		}
	}

	public void stop() {
		this.current.interrupt();
		this.current = null;
	}

	protected void close() {
		try {
			this.output.flush();
			this.output.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		try {
			this.output.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		try {
			this.socket.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void run() {
		int c = 0;
		try {
			String prompt = this.controller.getPrompt(this.sessionState);
			this.output.write(prompt.getBytes());
			this.output.flush();
			this.off = prompt.length();
			this.minOff = this.off;
			this.newLine = false;
		} catch (IOException e) {
			e.printStackTrace();
			stop();
		}
		do
			try {
				c = this.input.read();

				if (!handleTelnetSpecific(c)) {
					if (c == 13) {
						this.output.write(c);
						this.output.flush();
						continue;
					}
					if (c == 10) {
						this.output.write(c);
						this.output.flush();
						this.newLine = true;

						this.controller.notifyNewLine(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
						this.currentBuffer.setLength(0);
					} else if (c == 3) {
						this.controller.notifyControlChar(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff, 'c');
					} else if ((c == 8) || (c == 127)) {
						if (this.off > this.minOff) {
							if (!this.suppressOutput) {
								this.output.write(c);
								this.output.write(32);
								this.output.write(c);
							}
							this.off -= 1;
							if (this.currentBuffer.length() > 0)
								this.currentBuffer.setLength(this.currentBuffer
										.length() - 1);
						} else {
							this.newLine = false;
						}
					} else if (c == 27) {
						this.escape = true;
					} else if ((c == 91) && (this.escape)) {
						this.escapeSequence = true;
						this.escape = false;
					} else if ((c == 65) && (this.escapeSequence)) {
						this.escapeSequence = false;
						this.controller.notifyUp(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
					} else if ((c == 66) && (this.escapeSequence)) {
						this.escapeSequence = false;
						this.controller.notifyDown(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
					} else if ((c == 67) && (this.escapeSequence)) {
						this.escapeSequence = false;
						this.controller.notifyRight(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
					} else if ((c == 68) && (this.escapeSequence)) {
						this.escapeSequence = false;
						this.controller.notifyLeft(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
					} else if (c == 9) {
						this.controller.notifyTab(this.sessionState,
								this.currentBuffer.toString(), this.off
										- this.minOff);
					} else {
						this.currentBuffer.append((char) c);
						if (!this.suppressOutput) {
							this.output.write(c);
							this.output.flush();
						}
						this.off += 1;
						this.newLine = false;
					}

					if (this.newLine) {
						String prompt = this.controller
								.getPrompt(this.sessionState);
						this.newLine = false;
						this.output.write(prompt.getBytes());
						this.output.flush();
						this.off = prompt.length();
						this.minOff = this.off;
					}
				}
				this.output.write(TELNET_SEQUENCE_GOAHEAD);
				this.output.flush();
			} catch (IOException e) {
				break;
			}
		while ((c >= 0) && (this.sessionState.isSessionActive())
				&& (Thread.currentThread() == this.current));

		stop();
		close();
	}

	protected void setupTelnet() throws IOException {
		this.output.write(new byte[] { -1, -2, 1 });

		this.output.write(new byte[] { -1, -4, 33 });

		this.output.write(new byte[] { -1, -4, 34 });

		this.output.write(new byte[] { -1, -6, 34, 1, 0, -1, -16 });

		this.output.write(new byte[] { -1, -5, 31 });

		this.output.write(TELNET_SEQUENCE_GOAHEAD);
		this.output.flush();
	}

	protected boolean handleTelnetSpecific(int c) throws IOException {
		if (c == -1)
			return false;
		switch (this.telnetState) {
		case 0:
			if (c == 255) {
				this.telnetState = 1;
				return true;
			}

			return false;
		case 1:
			switch (c) {
			case 250:
				this.telnetState = 2;
				return true;
			case 240:
				this.telnetState = 0;
				return true;
			case 251:
				this.telnetState = 4;
				return true;
			case 252:
				this.telnetState = 5;
				return true;
			case 254:
				this.telnetState = 6;
				return true;
			case 253:
				this.telnetState = 7;
				return true;
			case 249:
				this.telnetState = 0;
				return true;
			case 246:
				this.telnetState = 0;
				return true;
			case 241:
			case 242:
			case 243:
			case 244:
			case 245:
			case 247:
			case 248:
			}
			return true;
		case 4:
			handleTelnetSubnegotiationWill(c);
			this.telnetState = 0;
			return true;
		case 5:
			handleTelnetSubnegotiationDo(c);
			this.telnetState = 0;
			return true;
		case 6:
			handleTelnetSubnegotiationDont(c);
			this.telnetState = 0;
			return true;
		case 7:
			handleTelnetSubnegotiationWont(c);
			this.telnetState = 0;
			return true;
		case 2:
			this.command = c;
			this.subnegotiationOptionPart = -1;
			this.telnetState = 3;
			return true;
		case 3:
			switch (this.command) {
			case 31:
				this.subnegotiationOptionPart += 1;
				if (this.subnegotiationOptionPart == 1) {
					this.printConfiguration.setRightMargin(c);
				} else if (this.subnegotiationOptionPart == 3) {
					this.telnetState = 0;
				}
				return true;
			case 24:
				this.subnegotiationOptionPart += 1;
				if (c != 255)
					break;
				this.telnetState = 1;
			}

			return true;
		}
		this.telnetState = 0;
		return false;
	}

	protected void handleTelnetSubnegotiationWill(int option)
			throws IOException {
		int pos = dos.indexOf(option);
		if (pos < 0) {
			this.output.write(new byte[] { -1, -2, (byte) option });
		} else {
			switch (option) {
			case 33:
				this.output.write(new byte[] { -1, -6, 33, 0, -1, -16 });

				break;
			default:
				this.output.write(new byte[] { -1, -4, (byte) option });
			}
		}
	}

	protected void handleTelnetSubnegotiationDo(int option) throws IOException {
		switch (option) {
		case 31:
			this.output.write(new byte[] { -1, -6, 31, 0,
					(byte) this.printConfiguration.getRightMargin(), 0, 30, -1,
					-16 });

			return;
		case 1:
			this.output.write(new byte[] { -1, -5, 1 });

			return;
		case 3:
			this.output.write(new byte[] { -1, -5, 3 });

			return;
		case 33:
			this.output.write(new byte[] { -1, -6, 33, 0, -1, -16 });

			return;
		case 34:
			this.output.write(new byte[] { -1, -6, 34, 1, 0, -1, -16 });

			return;
		case 24:
			this.output.write(new byte[] { -1, -6, 24, 0, 68, 69, 67, 45, 86,
					84, 49, 48, 48, -1, -16 });

			return;
		}
		this.output.write(new byte[] { -1, -3, (byte) option });
	}

	protected void handleTelnetSubnegotiationDont(int c) throws IOException {
		this.output.write(new byte[] { -1, -3, (byte) c });
	}

	protected void handleTelnetSubnegotiationWont(int c) throws IOException {
		this.output.write(new byte[] { -1, -2, (byte) c });
	}

	class TelnetSessionCallbackImpl implements TelnetSessionCallback {
		TelnetSessionCallbackImpl() {
		}

		public TelnetPrintConfiguration getTelnetPrintUtilities() {
			return TelnetSession.this.printConfiguration;
		}

		public void writeln(String message) throws IOException {
			write(message, 0);
			writeNewLine();
		}

		public void write(String txt, int offsetUpdateMask) throws IOException {
			TelnetSession.this.output.write(txt.getBytes());
			TelnetSession.this.output.flush();
			switch (offsetUpdateMask) {
			case 1:
				TelnetSession.this.minOff = TelnetSession.this.off;
				TelnetSession.this.off += txt.length();
				break;
			case 2:
				TelnetSession.this.off += txt.length();
				TelnetSession.this.minOff = TelnetSession.this.off;
				break;
			case 0:
				TelnetSession.this.off += txt.length();
				break;
			default:
				TelnetSession.this.off += txt.length();
			}
		}

		public void writeNewLine() throws IOException {
			TelnetSession.this.output
					.write(TelnetSession.this.printConfiguration.getEOLN()
							.getBytes());
			TelnetSession.this.output.flush();
			TelnetSession.this.newLine = true;
			TelnetSession.this.minOff = 0;
			TelnetSession.this.off = 0;
		}

		public void setSuppressOutput(boolean suppressOutput) {
			TelnetSession.this.suppressOutput = suppressOutput;
		}
	}

}
